/* *
 *
 *  (c) 2009-2017 Highsoft, Black Label
 *
 *  License: www.highcharts.com/license
 *
 * */

"use strict";

import H from "../parts/Globals.js";

import U from "../parts/Utilities.js";
var defined = U.defined,
  destroyObjectProperties = U.destroyObjectProperties,
  erase = U.erase,
  extend = U.extend,
  pick = U.pick,
  splat = U.splat,
  wrap = U.wrap;

import "../parts/Chart.js";
import controllableMixin from "./controllable/controllableMixin.js";
import ControllableRect from "./controllable/ControllableRect.js";
import ControllableCircle from "./controllable/ControllableCircle.js";
import ControllablePath from "./controllable/ControllablePath.js";
import ControllableImage from "./controllable/ControllableImage.js";
import ControllableLabel from "./controllable/ControllableLabel.js";
import eventEmitterMixin from "./eventEmitterMixin.js";
import MockPoint from "./MockPoint.js";
import ControlPoint from "./ControlPoint.js";

var merge = H.merge,
  addEvent = H.addEvent,
  fireEvent = H.fireEvent,
  find = H.find,
  reduce = H.reduce,
  chartProto = H.Chart.prototype;

/* *********************************************************************
 *
 * ANNOTATION
 *
 ******************************************************************** */

/**
 * @typedef {
 *          Annotation.ControllableCircle|
 *          Annotation.ControllableImage|
 *          Annotation.ControllablePath|
 *          Annotation.ControllableRect
 *          }
 *          Annotation.Shape
 */

/**
 * @typedef {Annotation.ControllableLabel} Annotation.Label
 */

/**
 * An annotation class which serves as a container for items like labels or
 * shapes. Created items are positioned on the chart either by linking them to
 * existing points or created mock points
 *
 * @class
 * @name Highcharts.Annotation
 *
 * @param {Highcharts.Chart} chart a chart instance
 * @param {Highcharts.AnnotationsOptions} userOptions the options object
 */
var Annotation = (H.Annotation = function (chart, userOptions) {
  var labelsAndShapes;

  /**
   * The chart that the annotation belongs to.
   *
   * @type {Highcharts.Chart}
   */
  this.chart = chart;

  /**
   * The array of points which defines the annotation.
   *
   * @type {Array<Highcharts.Point>}
   */
  this.points = [];

  /**
   * The array of control points.
   *
   * @type {Array<Annotation.ControlPoint>}
   */
  this.controlPoints = [];

  this.coll = "annotations";

  /**
   * The array of labels which belong to the annotation.
   *
   * @type {Array<Annotation.Label>}
   */
  this.labels = [];

  /**
   * The array of shapes which belong to the annotation.
   *
   * @type {Array<Annotation.Shape>}
   */
  this.shapes = [];

  /**
   * The options for the annotations.
   *
   * @type {Highcharts.AnnotationsOptions}
   */
  this.options = merge(this.defaultOptions, userOptions);

  /**
   * The user options for the annotations.
   *
   * @type {Highcharts.AnnotationsOptions}
   */
  this.userOptions = userOptions;

  // Handle labels and shapes - those are arrays
  // Merging does not work with arrays (stores reference)
  labelsAndShapes = this.getLabelsAndShapesOptions(this.options, userOptions);
  this.options.labels = labelsAndShapes.labels;
  this.options.shapes = labelsAndShapes.shapes;

  /**
   * The callback that reports to the overlapping-labels module which
   * labels it should account for.
   *
   * @name labelCollector
   * @memberOf Annotation#
   * @type {Function}
   */

  /**
   * The group svg element.
   *
   * @name group
   * @memberOf Annotation#
   * @type {Highcharts.SVGElement}
   */

  /**
   * The group svg element of the annotation's shapes.
   *
   * @name shapesGroup
   * @memberOf Annotation#
   * @type {Highcharts.SVGElement}
   */

  /**
   * The group svg element of the annotation's labels.
   *
   * @name labelsGroup
   * @memberOf Annotation#
   * @type {Highcharts.SVGElement}
   */

  this.init(chart, this.options);
});

merge(
  true,
  Annotation.prototype,
  controllableMixin,
  eventEmitterMixin,
  /** @lends Annotation# */
  {
    /**
     * List of events for `annotation.options.events` that should not be
     * added to `annotation.graphic` but to the `annotation`.
     *
     * @type {Array<string>}
     */
    nonDOMEvents: ["add", "afterUpdate", "drag", "remove"],

    /**
     * A basic type of an annotation. It allows to add custom labels
     * or shapes. The items  can be tied to points, axis coordinates
     * or chart pixel coordinates.
     *
     * @sample highcharts/annotations/basic/
     *         Basic annotations
     * @sample highcharts/demo/annotations/
     *         Advanced annotations
     * @sample highcharts/css/annotations
     *         Styled mode
     * @sample highcharts/annotations-advanced/controllable
     *         Controllable items
     * @sample {highstock} stock/annotations/fibonacci-retracements
     *         Custom annotation, Fibonacci retracement
     *
     * @type         {Array<*>}
     * @since        6.0.0
     * @requires     modules/annotations
     * @optionparent annotations
     */
    defaultOptions: {
      /**
       * Sets an ID for an annotation. Can be user later when removing an
       * annotation in [Chart#removeAnnotation(id)](
       * /class-reference/Highcharts.Chart#removeAnnotation) method.
       *
       * @type      {string|number}
       * @apioption annotations.id
       */

      /**
       * Whether the annotation is visible.
       *
       * @sample highcharts/annotations/visible/
       *         Set annotation visibility
       */
      visible: true,

      /**
       * Allow an annotation to be draggable by a user. Possible
       * values are `"x"`, `"xy"`, `"y"` and `""` (disabled).
       *
       * @sample highcharts/annotations/draggable/
       *         Annotations draggable: 'xy'
       *
       * @type       {string}
       * @validvalue ["x", "xy", "y", ""]
       */
      draggable: "xy",

      /**
       * Options for annotation's labels. Each label inherits options
       * from the labelOptions object. An option from the labelOptions
       * can be overwritten by config for a specific label.
       *
       * @requires modules/annotations
       */
      labelOptions: {
        /**
         * The alignment of the annotation's label. If right,
         * the right side of the label should be touching the point.
         *
         * @sample highcharts/annotations/label-position/
         *         Set labels position
         *
         * @type {Highcharts.AlignValue}
         */
        align: "center",

        /**
         * Whether to allow the annotation's labels to overlap.
         * To make the labels less sensitive for overlapping,
         * the can be set to 0.
         *
         * @sample highcharts/annotations/tooltip-like/
         *         Hide overlapping labels
         */
        allowOverlap: false,

        /**
         * The background color or gradient for the annotation's label.
         *
         * @sample highcharts/annotations/label-presentation/
         *         Set labels graphic options
         *
         * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
         */
        backgroundColor: "rgba(0, 0, 0, 0.75)",

        /**
         * The border color for the annotation's label.
         *
         * @sample highcharts/annotations/label-presentation/
         *         Set labels graphic options
         *
         * @type {Highcharts.ColorString}
         */
        borderColor: "black",

        /**
         * The border radius in pixels for the annotaiton's label.
         *
         * @sample highcharts/annotations/label-presentation/
         *         Set labels graphic options
         */
        borderRadius: 3,

        /**
         * The border width in pixels for the annotation's label
         *
         * @sample highcharts/annotations/label-presentation/
         *         Set labels graphic options
         */
        borderWidth: 1,

        /**
         * A class name for styling by CSS.
         *
         * @sample highcharts/css/annotations
         *         Styled mode annotations
         *
         * @since 6.0.5
         */
        className: "",

        /**
         * Whether to hide the annotation's label
         * that is outside the plot area.
         *
         * @sample highcharts/annotations/label-crop-overflow/
         *         Crop or justify labels
         */
        crop: false,

        /**
         * The label's pixel distance from the point.
         *
         * @sample highcharts/annotations/label-position/
         *         Set labels position
         *
         * @type      {number}
         * @apioption annotations.labelOptions.distance
         */

        /**
         * A
         * [format](https://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting)
         * string for the data label.
         *
         * @see [plotOptions.series.dataLabels.format](plotOptions.series.dataLabels.format.html)
         *
         * @sample highcharts/annotations/label-text/
         *         Set labels text
         *
         * @type      {string}
         * @apioption annotations.labelOptions.format
         */

        /**
         * Alias for the format option.
         *
         * @see [format](annotations.labelOptions.format.html)
         *
         * @sample highcharts/annotations/label-text/
         *         Set labels text
         *
         * @type      {string}
         * @apioption annotations.labelOptions.text
         */

        /**
         * Callback JavaScript function to format the annotation's
         * label. Note that if a `format` or `text` are defined, the
         * format or text take precedence and the formatter is ignored.
         * `This` refers to a point object.
         *
         * @sample highcharts/annotations/label-text/
         *         Set labels text
         *
         * @type    {Highcharts.FormatterCallbackFunction<Highcharts.Point>}
         * @default function () { return defined(this.y) ? this.y : 'Annotation label'; }
         */
        formatter: function () {
          return defined(this.y) ? this.y : "Annotation label";
        },

        /**
         * How to handle the annotation's label that flow outside the
         * plot area. The justify option aligns the label inside the
         * plot area.
         *
         * @sample highcharts/annotations/label-crop-overflow/
         *         Crop or justify labels
         *
         * @validvalue ["allow", "justify"]
         */
        overflow: "justify",

        /**
         * When either the borderWidth or the backgroundColor is set,
         * this    is the padding within the box.
         *
         * @sample highcharts/annotations/label-presentation/
         *         Set labels graphic options
         */
        padding: 5,

        /**
         * The shadow of the box. The shadow can be an object
         * configuration containing `color`, `offsetX`, `offsetY`,
         * `opacity` and `width`.
         *
         * @sample highcharts/annotations/label-presentation/
         *         Set labels graphic options
         *
         * @type {boolean|Highcharts.ShadowOptionsObject}
         */
        shadow: false,

        /**
         * The name of a symbol to use for the border around the label.
         * Symbols are predefined functions on the Renderer object.
         *
         * @sample highcharts/annotations/shapes/
         *         Available shapes for labels
         */
        shape: "callout",

        /**
         * Styles for the annotation's label.
         *
         * @see [plotOptions.series.dataLabels.style](plotOptions.series.dataLabels.style.html)
         *
         * @sample highcharts/annotations/label-presentation/
         *         Set labels graphic options
         *
         * @type {Highcharts.CSSObject}
         */
        style: {
          /** @ignore */
          fontSize: "11px",
          /** @ignore */
          fontWeight: "normal",
          /** @ignore */
          color: "contrast",
        },

        /**
         * Whether to [use HTML](https://www.highcharts.com/docs/chart-concepts/labels-and-string-formatting#html)
         * to render the annotation's label.
         */
        useHTML: false,

        /**
         * The vertical alignment of the annotation's label.
         *
         * @sample highcharts/annotations/label-position/
         *         Set labels position
         *
         * @type {Highcharts.VerticalAlignValue}
         */
        verticalAlign: "bottom",

        /**
         * The x position offset of the label relative to the point.
         * Note that if a `distance` is defined, the distance takes
         * precedence over `x` and `y` options.
         *
         * @sample highcharts/annotations/label-position/
         *         Set labels position
         */
        x: 0,

        /**
         * The y position offset of the label relative to the point.
         * Note that if a `distance` is defined, the distance takes
         * precedence over `x` and `y` options.
         *
         * @sample highcharts/annotations/label-position/
         *         Set labels position
         */
        y: -16,
      },

      /**
       * An array of labels for the annotation. For options that apply to
       * multiple labels, they can be added to the
       * [labelOptions](annotations.labelOptions.html).
       *
       * @type      {Array<*>}
       * @extends   annotations.labelOptions
       * @apioption annotations.labels
       */

      /**
       * This option defines the point to which the label will be
       * connected. It can be either the point which exists in the
       * series - it is referenced by the point's id - or a new point with
       * defined x, y properties and optionally axes.
       *
       * @sample highcharts/annotations/mock-point/
       *         Attach annotation to a mock point
       *
       * @type      {string|Highcharts.MockPointOptionsObject}
       * @requires  modules/annotations
       * @apioption annotations.labels.point
       */

      /**
       * The x position of the point. Units can be either in axis
       * or chart pixel coordinates.
       *
       * @type      {number}
       * @apioption annotations.labels.point.x
       */

      /**
       * The y position of the point. Units can be either in axis
       * or chart pixel coordinates.
       *
       * @type      {number}
       * @apioption annotations.labels.point.y
       */

      /**
       * This number defines which xAxis the point is connected to. It
       * refers to either the axis id or the index of the axis in the
       * xAxis array. If the option is not configured or the axis is not
       * found the point's x coordinate refers to the chart pixels.
       *
       * @type      {number|string}
       * @apioption annotations.labels.point.xAxis
       */

      /**
       * This number defines which yAxis the point is connected to. It
       * refers to either the axis id or the index of the axis in the
       * yAxis array. If the option is not configured or the axis is not
       * found the point's y coordinate refers to the chart pixels.
       *
       * @type      {number|string}
       * @apioption annotations.labels.point.yAxis
       */

      /**
       * An array of shapes for the annotation. For options that apply to
       * multiple shapes, then can be added to the
       * [shapeOptions](annotations.shapeOptions.html).
       *
       * @type      {Array<*>}
       * @extends   annotations.shapeOptions
       * @apioption annotations.shapes
       */

      /**
       * This option defines the point to which the shape will be
       * connected. It can be either the point which exists in the
       * series - it is referenced by the point's id - or a new point with
       * defined x, y properties and optionally axes.
       *
       * @type      {string|Highcharts.MockPointOptionsObject}
       * @extends   annotations.labels.point
       * @apioption annotations.shapes.point
       */

      /**
       * An array of points for the shape. This option is available for
       * shapes which can use multiple points such as path. A point can be
       * either a point object or a point's id.
       *
       * @see [annotations.shapes.point](annotations.shapes.point.html)
       *
       * @type      {Array<string|Highcharts.MockPointOptionsObject>}
       * @extends   annotations.labels.point
       * @apioption annotations.shapes.points
       */

      /**
       * Id of the marker which will be drawn at the final vertex of the
       * path. Custom markers can be defined in defs property.
       *
       * @see [defs.markers](defs.markers.html)
       *
       * @sample highcharts/annotations/custom-markers/
       *         Define a custom marker for annotations
       *
       * @type      {string}
       * @apioption annotations.shapes.markerEnd
       */

      /**
       * Id of the marker which will be drawn at the first vertex of the
       * path. Custom markers can be defined in defs property.
       *
       * @see [defs.markers](defs.markers.html)
       *
       * @sample {highcharts} highcharts/annotations/custom-markers/
       *         Define a custom marker for annotations
       *
       * @type      {string}
       * @apioption annotations.shapes.markerStart
       */

      /**
       * Options for annotation's shapes. Each shape inherits options from
       * the shapeOptions object. An option from the shapeOptions can be
       * overwritten by config for a specific shape.
       *
       * @requires  modules/annotations
       */
      shapeOptions: {
        /**
         * The width of the shape.
         *
         * @sample highcharts/annotations/shape/
         *         Basic shape annotation
         *
         * @type      {number}
         * @apioption annotations.shapeOptions.width
         **/

        /**
         * The height of the shape.
         *
         * @sample highcharts/annotations/shape/
         *         Basic shape annotation
         *
         * @type      {number}
         * @apioption annotations.shapeOptions.height
         */

        /**
         * The type of the shape, e.g. circle or rectangle.
         *
         * @sample highcharts/annotations/shape/
         *         Basic shape annotation
         *
         * @type      {string}
         * @default   'rect'
         * @apioption annotations.shapeOptions.type
         */

        /**
         * The color of the shape's stroke.
         *
         * @sample highcharts/annotations/shape/
         *         Basic shape annotation
         *
         * @type {Highcharts.ColorString}
         */
        stroke: "rgba(0, 0, 0, 0.75)",

        /**
         * The pixel stroke width of the shape.
         *
         * @sample highcharts/annotations/shape/
         *         Basic shape annotation
         */
        strokeWidth: 1,

        /**
         * The color of the shape's fill.
         *
         * @sample highcharts/annotations/shape/
         *         Basic shape annotation
         *
         * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject}
         */
        fill: "rgba(0, 0, 0, 0.75)",

        /**
         * The radius of the shape.
         *
         * @sample highcharts/annotations/shape/
         *         Basic shape annotation
         */
        r: 0,

        /**
         * Defines additional snapping area around an annotation
         * making this annotation to focus. Defined in pixels.
         */
        snap: 2,
      },

      /**
       * Options for annotation's control points. Each control point
       * inherits options from controlPointOptions object.
       * Options from the controlPointOptions can be overwritten
       * by options in a specific control point.
       *
       * @type      {Annotation.ControlPoint.Options}
       * @requires  modules/annotations
       * @apioption annotations.controlPointOptions
       */
      controlPointOptions: {
        /**
         * @function {Annotation.ControlPoint.Positioner}
         * @apioption annotations.controlPointOptions.positioner
         */

        symbol: "circle",
        width: 10,
        height: 10,
        style: {
          stroke: "black",
          "stroke-width": 2,
          fill: "white",
        },
        visible: false,
        events: {},
      },

      /**
       * Event callback when annotation is added to the chart.
       *
       * @type      {Highcharts.EventCallbackFunction<Highcharts.Annotation>}
       * @since     7.1.0
       * @apioption annotations.events.add
       */

      /**
       * Event callback when annotation is updated (e.g. drag and
       * droppped or resized by control points).
       *
       * @type      {Highcharts.EventCallbackFunction<Highcharts.Annotation>}
       * @since     7.1.0
       * @apioption annotations.events.afterUpdate
       */

      /**
       * Event callback when annotation is removed from the chart.
       *
       * @type      {Highcharts.EventCallbackFunction<Highcharts.Annotation>}
       * @since     7.1.0
       * @apioption annotations.events.remove
       */

      /**
       * Events available in annotations.
       *
       * @requires modules/annotations
       */
      events: {},

      /**
       * The Z index of the annotation.
       */
      zIndex: 6,
    },

    /**
     * Initialize the annotation.
     *
     * @param {Highcharts.Chart}
     *        The chart
     * @param {Highcharts.AnnotationsOptions}
     *        The user options for the annotation
     */
    init: function () {
      this.linkPoints();
      this.addControlPoints();
      this.addShapes();
      this.addLabels();
      this.addClipPaths();
      this.setLabelCollector();
    },

    getLabelsAndShapesOptions: function (baseOptions, newOptions) {
      var mergedOptions = {};

      ["labels", "shapes"].forEach(function (name) {
        if (baseOptions[name]) {
          mergedOptions[name] = splat(newOptions[name]).map(function (
            basicOptions,
            i
          ) {
            return merge(baseOptions[name][i], basicOptions);
          });
        }
      });

      return mergedOptions;
    },

    addShapes: function () {
      (this.options.shapes || []).forEach(function (shapeOptions, i) {
        var shape = this.initShape(shapeOptions, i);

        merge(true, this.options.shapes[i], shape.options);
      }, this);
    },

    addLabels: function () {
      (this.options.labels || []).forEach(function (labelsOptions, i) {
        var labels = this.initLabel(labelsOptions, i);

        merge(true, this.options.labels[i], labels.options);
      }, this);
    },

    addClipPaths: function () {
      this.setClipAxes();

      if (this.clipXAxis && this.clipYAxis) {
        this.clipRect = this.chart.renderer.clipRect(this.getClipBox());
      }
    },

    setClipAxes: function () {
      var xAxes = this.chart.xAxis,
        yAxes = this.chart.yAxis,
        linkedAxes = reduce(
          (this.options.labels || []).concat(this.options.shapes || []),
          function (axes, labelOrShape) {
            return [
              xAxes[
                labelOrShape && labelOrShape.point && labelOrShape.point.xAxis
              ] || axes[0],
              yAxes[
                labelOrShape && labelOrShape.point && labelOrShape.point.yAxis
              ] || axes[1],
            ];
          },
          []
        );

      this.clipXAxis = linkedAxes[0];
      this.clipYAxis = linkedAxes[1];
    },

    getClipBox: function () {
      return {
        x: this.clipXAxis.left,
        y: this.clipYAxis.top,
        width: this.clipXAxis.width,
        height: this.clipYAxis.height,
      };
    },

    setLabelCollector: function () {
      var annotation = this;

      annotation.labelCollector = function () {
        return annotation.labels.reduce(function (labels, label) {
          if (!label.options.allowOverlap) {
            labels.push(label.graphic);
          }

          return labels;
        }, []);
      };

      annotation.chart.labelCollectors.push(annotation.labelCollector);
    },

    /**
     * Set an annotation options.
     *
     * @param {Highcharts.AnnotationsOptions} - user options for an annotation
     */
    setOptions: function (userOptions) {
      this.options = merge(this.defaultOptions, userOptions);
    },

    redraw: function (animation) {
      this.linkPoints();

      if (!this.graphic) {
        this.render();
      }

      if (this.clipRect) {
        this.clipRect.animate(this.getClipBox());
      }

      this.redrawItems(this.shapes, animation);
      this.redrawItems(this.labels, animation);

      controllableMixin.redraw.call(this, animation);
    },

    /**
     * @param {Array<(Annotation.Label|Annotation.Shape)>} items
     * @param {boolean} [animation]
     */
    redrawItems: function (items, animation) {
      var i = items.length;

      // needs a backward loop
      // labels/shapes array might be modified
      // due to destruction of the item
      while (i--) {
        this.redrawItem(items[i], animation);
      }
    },

    render: function () {
      var renderer = this.chart.renderer;

      this.graphic = renderer
        .g("annotation")
        .attr({
          zIndex: this.options.zIndex,
          visibility: this.options.visible ? "visible" : "hidden",
        })
        .add();

      this.shapesGroup = renderer
        .g("annotation-shapes")
        .add(this.graphic)
        .clip(this.chart.plotBoxClip);

      this.labelsGroup = renderer
        .g("annotation-labels")
        .attr({
          // hideOverlappingLabels requires translation
          translateX: 0,
          translateY: 0,
        })
        .add(this.graphic);

      if (this.clipRect) {
        this.graphic.clip(this.clipRect);
      }

      this.addEvents();

      controllableMixin.render.call(this);
    },

    /**
     * Set the annotation's visibility.
     *
     * @param {Boolean} [visible] - Whether to show or hide an annotation.
     * If the param is omitted, the annotation's visibility is toggled.
     */
    setVisibility: function (visibility) {
      var options = this.options,
        visible = pick(visibility, !options.visible);

      this.graphic.attr("visibility", visible ? "visible" : "hidden");

      if (!visible) {
        this.setControlPointsVisibility(false);
      }

      options.visible = visible;
    },

    setControlPointsVisibility: function (visible) {
      var setItemControlPointsVisibility = function (item) {
        item.setControlPointsVisibility(visible);
      };

      controllableMixin.setControlPointsVisibility.call(this, visible);

      this.shapes.forEach(setItemControlPointsVisibility);
      this.labels.forEach(setItemControlPointsVisibility);
    },

    /**
     * Destroy the annotation. This function does not touch the chart
     * that the annotation belongs to (all annotations are kept in
     * the chart.annotations array) - it is recommended to use
     * {@link Highcharts.Chart#removeAnnotation} instead.
     */
    destroy: function () {
      var chart = this.chart,
        destroyItem = function (item) {
          item.destroy();
        };

      this.labels.forEach(destroyItem);
      this.shapes.forEach(destroyItem);

      this.clipXAxis = null;
      this.clipYAxis = null;

      erase(chart.labelCollectors, this.labelCollector);

      eventEmitterMixin.destroy.call(this);
      controllableMixin.destroy.call(this);

      destroyObjectProperties(this, chart);
    },

    /**
     * See {@link Highcharts.Chart#removeAnnotation}.
     */
    remove: function () {
      // Let chart.update() remove annoations on demand
      return this.chart.removeAnnotation(this);
    },

    update: function (userOptions) {
      var chart = this.chart,
        labelsAndShapes = this.getLabelsAndShapesOptions(
          this.userOptions,
          userOptions
        ),
        userOptionsIndex = chart.annotations.indexOf(this),
        options = H.merge(true, this.userOptions, userOptions);

      options.labels = labelsAndShapes.labels;
      options.shapes = labelsAndShapes.shapes;

      this.destroy();
      this.constructor(chart, options);

      // Update options in chart options, used in exporting (#9767):
      chart.options.annotations[userOptionsIndex] = options;

      this.isUpdating = true;
      this.redraw();
      this.isUpdating = false;
      fireEvent(this, "afterUpdate");
    },

    /* *************************************************************
     * ITEM SECTION
     * Contains methods for handling a single item in an annotation
     **************************************************************** */

    /**
     * Initialisation of a single shape
     *
     * @param {Object} shapeOptions - a confg object for a single shape
     */
    initShape: function (shapeOptions, index) {
      var options = merge(
          this.options.shapeOptions,
          {
            controlPointOptions: this.options.controlPointOptions,
          },
          shapeOptions
        ),
        shape = new Annotation.shapesMap[options.type](this, options, index);

      shape.itemType = "shape";

      this.shapes.push(shape);

      return shape;
    },

    /**
     * Initialisation of a single label
     *
     * @param {Object} labelOptions
     **/
    initLabel: function (labelOptions, index) {
      var options = merge(
          this.options.labelOptions,
          {
            controlPointOptions: this.options.controlPointOptions,
          },
          labelOptions
        ),
        label = new ControllableLabel(this, options, index);

      label.itemType = "label";

      this.labels.push(label);

      return label;
    },

    /**
     * Redraw a single item.
     *
     * @param {Annotation.Label|Annotation.Shape} item
     * @param {boolean} [animation]
     */
    redrawItem: function (item, animation) {
      item.linkPoints();

      if (!item.shouldBeDrawn()) {
        this.destroyItem(item);
      } else {
        if (!item.graphic) {
          this.renderItem(item);
        }

        item.redraw(pick(animation, true) && item.graphic.placed);

        if (item.points.length) {
          this.adjustVisibility(item);
        }
      }
    },

    /**
     * Hide or show annotaiton attached to points.
     *
     * @param {Annotation.Label|Annotation.Shape} item
     */

    adjustVisibility: function (item) {
      // #9481
      var hasVisiblePoints = false,
        label = item.graphic;

      item.points.forEach(function (point) {
        if (point.series.visible !== false && point.visible !== false) {
          hasVisiblePoints = true;
        }
      });

      if (!hasVisiblePoints) {
        label.hide();
      } else if (label.visibility === "hidden") {
        label.show();
      }
    },

    /**
     * Destroy a single item.
     *
     * @param {Annotation.Label|Annotation.Shape} item
     */
    destroyItem: function (item) {
      // erase from shapes or labels array
      erase(this[item.itemType + "s"], item);
      item.destroy();
    },

    /**
     * @private
     */
    renderItem: function (item) {
      item.render(
        item.itemType === "label" ? this.labelsGroup : this.shapesGroup
      );
    },
  }
);

/**
 * An object uses for mapping between a shape type and a constructor.
 * To add a new shape type extend this object with type name as a key
 * and a constructor as its value.
 */
Annotation.shapesMap = {
  rect: ControllableRect,
  circle: ControllableCircle,
  path: ControllablePath,
  image: ControllableImage,
};

Annotation.types = {};

Annotation.MockPoint = MockPoint;
Annotation.ControlPoint = ControlPoint;

H.extendAnnotation = function (
  Constructor,
  BaseConstructor,
  prototype,
  defaultOptions
) {
  BaseConstructor = BaseConstructor || Annotation;

  merge(true, Constructor.prototype, BaseConstructor.prototype, prototype);

  Constructor.prototype.defaultOptions = merge(
    Constructor.prototype.defaultOptions,
    defaultOptions || {}
  );
};

/* *********************************************************************
 *
 * EXTENDING CHART PROTOTYPE
 *
 ******************************************************************** */

extend(
  chartProto,
  /** @lends Highcharts.Chart# */ {
    initAnnotation: function (userOptions) {
      var Constructor = Annotation.types[userOptions.type] || Annotation,
        annotation = new Constructor(this, userOptions);

      this.annotations.push(annotation);

      return annotation;
    },

    /**
     * Add an annotation to the chart after render time.
     *
     * @param  {Highcharts.AnnotationsOptions} options
     *         The annotation options for the new, detailed annotation.
     * @param {boolean} [redraw]
     *
     * @return {Highcharts.Annotation} - The newly generated annotation.
     */
    addAnnotation: function (userOptions, redraw) {
      var annotation = this.initAnnotation(userOptions);

      this.options.annotations.push(annotation.options);

      if (pick(redraw, true)) {
        annotation.redraw();
      }

      return annotation;
    },

    /**
     * Remove an annotation from the chart.
     *
     * @param {String|Number|Annotation} idOrAnnotation - The annotation's id or
     *      direct annotation object.
     */
    removeAnnotation: function (idOrAnnotation) {
      var annotations = this.annotations,
        annotation =
          idOrAnnotation.coll === "annotations"
            ? idOrAnnotation
            : find(annotations, function (annotation) {
                return annotation.options.id === idOrAnnotation;
              });

      if (annotation) {
        fireEvent(annotation, "remove");
        erase(this.options.annotations, annotation.options);
        erase(annotations, annotation);
        annotation.destroy();
      }
    },

    drawAnnotations: function () {
      this.plotBoxClip.attr(this.plotBox);

      this.annotations.forEach(function (annotation) {
        annotation.redraw();
      });
    },
  }
);

// Let chart.update() update annotations
chartProto.collectionsWithUpdate.push("annotations");

// Let chart.update() create annoations on demand
chartProto.collectionsWithInit.annotations = [chartProto.addAnnotation];

chartProto.callbacks.push(function (chart) {
  chart.annotations = [];

  if (!chart.options.annotations) {
    chart.options.annotations = [];
  }

  chart.plotBoxClip = this.renderer.clipRect(this.plotBox);

  chart.controlPointsGroup = chart.renderer
    .g("control-points")
    .attr({ zIndex: 99 })
    .clip(chart.plotBoxClip)
    .add();

  chart.options.annotations.forEach(function (annotationOptions, i) {
    var annotation = chart.initAnnotation(annotationOptions);

    chart.options.annotations[i] = annotation.options;
  });

  chart.drawAnnotations();
  addEvent(chart, "redraw", chart.drawAnnotations);
  addEvent(chart, "destroy", function () {
    chart.plotBoxClip.destroy();
    chart.controlPointsGroup.destroy();
  });
});

wrap(H.Pointer.prototype, "onContainerMouseDown", function (proceed) {
  if (!this.chart.hasDraggedAnnotation) {
    proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  }
});
